/*
 * The RmiProcessManager is used to create, reference and manage running
 * processes.
 */

package common;

import java.io.IOException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 *
 */
public class RmiProcessManager extends UnicastRemoteObject
        implements IProcessManager
{
    // maximum number of processes
    private final int MAX_PROCESSES = 5;

    /*
     * this seomaphore is simply used to restrict access to
     * getRegisteredProcesses() method until registeredProcesses gets a val
     */
    private Semaphore getRegisteredProcessesSem = new Semaphore(0);
    private Semaphore availableListeningProcesses = new Semaphore(0, true);
    private ArrayList<Integer> registeredProcesses;
    // processes that do not talk to other process yet
    private ArrayList<Integer> muteProcesses = new ArrayList<Integer>();
    // processes that are not listening to another process
    // Key --talks-to--> Value
    private Map<Integer, Integer> talkingTo;

    public RmiProcessManager() throws RemoteException
    {
        registeredProcesses = new ArrayList<Integer>();
        talkingTo = new HashMap<Integer, Integer>();
    }
    
    public void addProcess(Integer process)
    {
        if (!registeredProcesses.contains(process))
        {
            registeredProcesses.add(process);
            getRegisteredProcessesSem.release();
            // this process doesn't yet talk to any other process
            muteProcesses.add(process);
            Logger.getLogger(RmiProcessManager.class.getName()).log(
                    Level.INFO, "Registered new process:" + process);
            updateProcessesRelationShips();
        }
    }

    /*
     * Makes processes couples (talker -> listener)
     */
    private synchronized void updateProcessesRelationShips()
    {
        Integer listeningProcess;
        Integer muteProcess;

        do
        {  /*
            * We don't wan't to get inconsistency if two calls come to
            * getListeningNeighbourProcess() at the same time
            */
            // we get the first added mute process
            muteProcess = muteProcesses.get(0);
            // we get the neighbour of this muteProcess
            listeningProcess = getListeningNeighbourProcess(muteProcess);

            if (listeningProcess != null)
            {
                talkingTo.put(muteProcess, listeningProcess);
                muteProcesses.remove(muteProcess);
                // release the one resource for the available listening process
                availableListeningProcesses.release();
            }
        }
        while ((!muteProcesses.isEmpty()) && (listeningProcess != null));
    }


    /**
     * TODO: review this peace of code, if it works for miletone 12 it'll be ok
     * @param process from which we want to get the listening neighbour
     * @return the listening neighbour of the given process or null if it hasn't
     */
    private Integer getListeningNeighbourProcess(Integer process)
    {
        Integer listeningNeighbourProcess = null;
        Integer listeningNeighbourProcessIndex = -1;
        Integer processIndex = registeredProcesses.indexOf(process);

        listeningNeighbourProcessIndex = (processIndex + 1);
        if (registeredProcesses.size() == MAX_PROCESSES)
        {
            /*
             * if the muteProcess is the last in the list, its neighbour is
             * the first in the list (circle)
             */
            listeningNeighbourProcessIndex =
                    listeningNeighbourProcessIndex % MAX_PROCESSES;
            listeningNeighbourProcess = registeredProcesses.get(
                        listeningNeighbourProcessIndex);
        }
        /*
         * if not all process were yet registered, we will have to wait
         * for a new neighbour to come
         */
        else if(listeningNeighbourProcessIndex < registeredProcesses.size())
        {
            listeningNeighbourProcess = registeredProcesses.get(
                        listeningNeighbourProcessIndex);
        }
        else
        {
            listeningNeighbourProcess = null;
        }

        return listeningNeighbourProcess;
    }

    /**
     * Each process has only one "friend" listening to him
     * This method will block if there're no process available to talk to
     * @return the id of the process it can talk to
     */
    public Integer onlyTalksTo(Integer process)
    {
        Integer listeningProcess;

        do
        {
            try
            {
                /*
                 * If that process already asked for a listening process
                 * then we don't need to change the semaphore.
                 */
                if (!talkingTo.containsKey(process))
                {
                    // waits until a listening process gets available
                    availableListeningProcesses.acquire();
                }
            } catch (InterruptedException ex)
            {
                Logger.getLogger(RmiProcessManager.class.getName()).log(
                        Level.SEVERE, null, ex);
            }
            listeningProcess = talkingTo.get(process);
        } while (listeningProcess == null);

        String debugString = process + " --talks-to--> " + listeningProcess;
        Logger.getLogger(RmiProcessManager.class.getName()).log(
                    Level.INFO, debugString );
        return listeningProcess;
    }


    /**
     *
     * @return registeredProcesses
     */
    public ArrayList<Integer> getRegisteredProcesses()
    {
        /*
         * Just a workaround to lock a process until registeredProcesses gets
         * a value
         */
        if (registeredProcesses.isEmpty())
        {
            try
            {
                getRegisteredProcessesSem.acquire();
            } catch (InterruptedException ex)
            {
                Logger.getLogger(RmiProcessManager.class.getName()).log(Level.SEVERE, null, ex);
            }
            getRegisteredProcessesSem.release();
        }

        return registeredProcesses;
    }

    public static void main(String args[]) {
        try
        {
            LocateRegistry.createRegistry(1099);
            Logger.getLogger(RmiProcessManager.class.getName()).log(
                    Level.INFO, "RMI registry created.");
        } catch (RemoteException ex) // the RMI registry already exists
        {
            Logger.getLogger(RmiProcessManager.class.getName()).log(
                    Level.INFO, "The RMI registry already exists.");
        }

        try
        {
            RmiProcessManager processManager = new RmiProcessManager();
            Naming.rebind("rmi://localhost:1099/" + Helper.PROCESS_MANAGER_RMI,
                    processManager);
        } catch (IOException ex)
        {
            Logger.getLogger(RmiProcessManager.class.getName()).log(
                    Level.SEVERE, null, ex);
        }
    }
}
